C#
Mateu has a C# server-side implementation (Mateu.NET). You annotate plain C# classes, and the existing renderers (web and native) render them with zero client changes — exactly as they render the Java backend.
Coming from Java? The Language Rosetta maps every declaration idiom side by side, and the parity matrix shows exactly what this server supports today.
How it works
Section titled “How it works”Every Mateu renderer speaks one protocol: POST /{baseUrl}/mateu/v3/sync/{route} in, a
UIIncrement JSON tree out. So the C# side does not re-implement the whole framework — it
emits the same JSON. An ASP.NET Core minimal API hosts the sync endpoint; a reflection mapper
turns your annotated classes into the Mateu component tree; System.Text.Json polymorphism produces
the type discriminators the renderers expect.
The implementation lives at backend/dotnet
(DESIGN.md for the plan, README.md for status).
Run it
Section titled “Run it”# .NET 8 SDK required (e.g. via https://dot.net/v1/dotnet-install.sh --channel 8.0)cd backend/dotnetdotnet run --project samples/Mateu.Demo # serves on http://0.0.0.0:8593dotnet test # golden-JSON testsPoint any Mateu renderer at it — e.g. set the Compose app’s mateu.baseUrl=http://localhost:8593.
The server binds to 0.0.0.0, so the iOS simulator (localhost:8593) and Android emulator
(10.0.2.2:8593) reach it too.
Projects
Section titled “Projects”| Project | Role |
|---|---|
src/Mateu.Uidl | Public API — attributes ([UI], [Title], [Button], …) + data types (Message) |
src/Mateu.Dtos | The wire model — UIIncrementDto, ComponentDto + metadata (polymorphism on type) |
src/Mateu.Core | The engine — MateuRegistry, ReflectionMapper, SyncHandler |
src/Mateu.AspNetCore | AddMateu() / MapMateu() — DI + the POST /mateu/v3/sync/{route} endpoint |
samples/Mateu.Demo | A runnable ASP.NET app |
test/Mateu.Tests | Golden-JSON tests asserting wire compatibility with the Java backend |
Wire it up in Program.cs:
var builder = WebApplication.CreateBuilder(args);builder.Services.AddMateu();var app = builder.Build();app.MapMateu();app.Run("http://0.0.0.0:8593");A [UI] class becomes a routed form. Public properties become fields (the type is inferred:
string, int/long → integer, double/decimal → number, bool → boolean, DateOnly/
DateTime → date, enum → dropdown). [Required] (from System.ComponentModel.DataAnnotations)
makes a field required and is enforced server-side. A method with [Button] returning a Message
shows a toast.
using Mateu.Uidl;using System.ComponentModel.DataAnnotations;
[UI("person"), Title("Person"), Subtitle("Personal data")]public class Person{ [Required, Section("Identity")] public string? Name { get; set; } public int Age { get; set; } [Section("Preferences")] public bool Subscribed { get; set; } public Role Role { get; set; } // enum → dropdown
[Button] public Message Save() => new($"Saved {Name}");}
public enum Role { Guest, Admin }[Section("…")] groups the fields that follow it into a titled card.
Derive from Crud<T> and override Fetch (and, as needed, Get/Save/Delete). You get a
searchable listing — a table on desktop, cards on mobile — plus full detail / edit / new flows
(routes /x, /x/{id}, /x/{id}/edit, /x/new), with [Required] validation on save and
back-to-list navigation.
[UI("reservations"), Title("Reservations")]public class Reservations : Crud<Reservation>{ public override IEnumerable<Reservation> Fetch(string? search) => Store.All.Where(r => search is null || r.Locator.Contains(search));
public override Reservation? Get(string id) => Store.ById(id); public override void Save(Reservation r) => Store.Put(r); public override void Delete(string id) => Store.Remove(id);}The listing renders a smart search bar whose filters come straight from the entity: enums
become multi-selects (IN over the picked values), DateOnly/DateTime properties become from–to
date ranges, numerics annotated [RangeFilter] become min–max ranges, and strings/bools/plain
numbers keep single-value widgets. The values are applied automatically over what Fetch returns
— no filter code to write:
public class Reservation{ public string Id { get; set; } = ""; public string Guest { get; set; } = ""; public Channel Channel { get; set; } // multi-select filter public DateOnly Arrival { get; set; } // date-range filter [RangeFilter] public double Total { get; set; } // number-range filter}Override EditInDrawer => true and New/row clicks open the create/edit form in a drawer
sliding over the listing (the listing never unmounts; saving persists, closes the drawer and
refreshes the rows in place). EditDrawerWidth adjusts the panel width.
Two more archetypes mirror the Java orchestrators: CollectionDetail<TRow> (searchable card
list on the left, the selected item’s detail re-rendered in place on the right) and
GeneralOverview<TRow> (a record context switcher over the selected record’s overview). Both are
built on the fluent FormField primitive — a live field you can compose into any fluent tree.
Listings & capabilities
Section titled “Listings & capabilities”When you don’t want the whole CRUD pack, implement IListing<TRow> — one method — and grow the
page by declaring capabilities, exactly like the Java model:
[UI("/orders")]public class Orders : IListing<OrderRow>, ISearchable, IFilterable<OrderFilters>{ public ListingData<OrderRow> Search(SearchRequest request) { var text = request.SearchText; // filled because ISearchable var filters = request.Filters<OrderFilters>(); // typed, from IFilterable return ListingData.From(repo.Find(text, filters, request.Pageable)); }}A bare IListing<TRow> is just the table (sorting + pagination free). Then:
| Declare | Contract | You get |
|---|---|---|
ISearchable | — (marker) | the search box |
IFilterable<TFilters> | — (marker) | the filter bar, built reflectively from TFilters |
INavigable<TDetail, TId> | View(id) | clickable rows + the /:id detail page |
IEditable<TEditor, TId> | Edit(id) + Save(editor) | editing — in a drawer over the listing when not navigable |
ICreatable<TForm, TId> | CreationForm() + Create(form) | the New button + /new form |
IDeletable<TId> | DeleteAllById(ids) | row selection + the Delete button |
Capability methods take typed objects (the framework binds the submitted state for you —
Save(editor), Create(form)), the C# idiom of the port. ListingData.From(rows) sorts and
pages in memory; construct it with an explicit TotalElements to push paging to the database.
Crud<T> is the full pack — all capabilities at once, restricted with [ReadOnly]/[NotCreatable]… —
and the virtual CanView/CanEdit/CanCreate/CanDelete hooks narrow it programmatically.
App shell & navigation
Section titled “App shell & navigation”An [App] class is the application shell; each [MenuItem] method contributes a menu entry that
navigates to the view it returns.
[App("My C# Mateu app")]public class DemoApp{ [MenuItem("Reservations")] public Reservations Reservations() => new(); [MenuItem("Person")] public Person Person() => new(); [MenuItem("Sign up")] public SignupWizard SignupWizard() => new();}Wizards
Section titled “Wizards”Derive from Wizard, tag each field with [Step(n)], and implement Complete(). Mateu renders a
progress bar plus Back/Next; the step + field values round-trip through component state. Annotate
the class with [WizardProgress("steps")] for connected step bullets, or
[WizardProgress("rail")] for the guided-process lateral rail (a sticky right band with a big
current | total counter over the vertical step list).
[UI("signup"), Title("Sign up")]public class SignupWizard : Wizard{ [Step(1)] public string? Email { get; set; } [Step(2), Password] public string? Password { get; set; } public override Message Complete() => new($"Welcome {Email}");}Page decorations
Section titled “Page decorations”[Subtitle("…")]— a subtitle under the page title.[Banner(BannerTheme.Info, "Title")]on a method — a banner below the header. Themes:Info,Success,Warning,Danger. If the method returns astring, that’s the banner description.[HeaderBadge(color: "success")]on a property — a status chip in the header strip (shown when the value is non-empty).
[Banner(BannerTheme.Info, "Heads up")] public string Note() => "Fields marked * are required";[HeaderBadge("success")] public string Status { get; set; } = "Active";Tag consecutive fields with [Tab("Name")] to group them into a tab strip (a TabLayout); the
first tab is active by default.
[Tab("Identity")] public string? Name { get; set; }[Tab("Identity"), Password] public string? Secret { get; set; }[Tab("Profile")] public string? Bio { get; set; }Field stereotypes
Section titled “Field stereotypes”| Attribute | Effect |
|---|---|
[Multiline] | renders as a multi-line text area |
[Password] | renders as a password input |
[Money] | tags the field money so the renderer formats it as currency |
[PlainText] | renders read-only plain text (also valid at class level for all fields) |
[Stereotype("…")] | sets an explicit stereotype |
[Multiline] public string? Notes { get; set; }[Money] public decimal Balance { get; set; }[PlainText] public string? MemberSince { get; set; }KPIs & floating action buttons
Section titled “KPIs & floating action buttons”[Kpi("Title")]on a (parameterless) method → a KPI card in the page header showing the method’s return value.[Fab("icon", "label", order)]on a method → a floating action button; clicking it invokes the method like any other action.
[Kpi("Open tickets")] public string OpenTickets() => "42";[Fab("plus", "Add", 0)] public Message Add() => new("Added");Keyboard shortcuts
Section titled “Keyboard shortcuts”Bind an action method to a shortcut with [Shortcut("ctrl+s")].
[Button, Shortcut("ctrl+s")] public Message Save() => new("Saved");Page flags
Section titled “Page flags”[Compact]— high-density rendering (condensed spacing) for information-dense screens.[ConfirmOnNavigationIfDirty]— warn before leaving the view with unsaved changes.
[UI("checkin"), Compact, ConfirmOnNavigationIfDirty]public class CheckIn { /* … */ }Dashboards, foldouts & fluent components
Section titled “Dashboards, foldouts & fluent components”The nine dashboard/UX component types are available as fluent records (in Mateu.Uidl):
MetricCard (with MetricTrend up/down/neutral), Scoreboard, DashboardPanel,
DashboardLayout, FoldoutLayout/FoldoutPanel, HeroSection, EmptyState, Skeleton
(text/card/grid/form variants) and Gantt/GanttTask — plus the generic Text, Button, Card,
HorizontalLayout, VerticalLayout and TabLayout/TabPanel building blocks. They serialize to
the exact wire shape of the Java DTOs, so the renderers draw the C# output unchanged.
Any view can return a fluent tree by implementing IComponentTreeSupplier, but the easiest path is
a declarative page archetype — derive from Dashboard, Foldout, Welcome or ItemOverview
and declare component-holding properties; [Panel] marks titled tiles / fold-out panels / tabs:
[UI("dashboard"), Title("Sales dashboard")]public class SalesDashboard : Dashboard // or Foldout, Welcome, ItemOverview{ // Consecutive MetricCard properties form a Scoreboard KPI band. public MetricCard Revenue { get; } = new() { Title = "Revenue", Value = "1.2", Unit = "M€", Trend = MetricTrend.Up, TrendLabel = "+12%", ActionId = "openRevenue" };
// [Panel] component properties become titled tiles on a responsive grid. [Panel(Title = "Delivery plan", ColSpan = 2)] public Gantt Plan { get; } = new() { Tasks = [ new GanttTask { Id = "t1", Title = "Build", Start = new(2026, 7, 1), End = new(2026, 8, 20), Progress = 40 } ] };
[Panel(Title = "Alerts")] public EmptyState Alerts { get; } = new() { Icon = "🎉", Title = "No alerts" };
public Message OpenRevenue() => new("Drill-in"); // MetricCard.ActionId dispatches this}Dashboard— MetricCards →Scoreboardband;[Panel]components →DashboardPaneltiles on aDashboardLayoutgrid (overrideColumnsto fix the count; 0 = auto-fit).Foldout— the first non-[Panel]component property is the always-visible overview;[Panel(Icon = …, Open = false)]properties are lateral fold-out panels.Welcome—Buttonproperties become CTAs inside a centeredHeroSection(overrideHeroTitle/HeroSubtitle/HeroImage);[Panel]properties are highlight tiles below.ItemOverview— the first non-[Panel]component property is the sticky key-info card (left);[Panel(Title = …)]properties become tabs on the right.
i18n, events & security
Section titled “i18n, events & security”- i18n — implement
ITranslatorand register it; titles, labels and menu entries are translated. - Events —
[Emits("event-name")]advertises an event a view emits;[SubscribeTo("event", "action")]runsactionwhen that event fires (anOnCustomEventtrigger). - Security —
[Secured("permission")]marks a view as requiring a permission; the[App]shell can carry login/logout URLs.
public class UpperTranslator : ITranslator{ public string Translate(string key) => key.ToUpperInvariant();}
[UI("orders"), Emits("order-created"), SubscribeTo("inventory-changed", "refresh")]public class Orders { /* … */ }Navigation links, radio groups & adaptive layout
Section titled “Navigation links, radio groups & adaptive layout”[LinkTo("/customers/${state.customerId}")] puts a navigation icon on a field (templates are
interpolated client-side); implement ILinkSupplier.Link(memberName) for runtime decisions.
[UseRadioButtons] forces an enum to render as a radio group, and [AutoLayout] enables the
adaptive layout inference (small enums become radios, long forms fold, section-heavy forms become
tabs) — the same heuristics as the Java server.
Application context selector
Section titled “Application context selector”An [AppContext] member of the app class becomes a selector on the app header that fixes a value
for EVERY screen (the active hotel, the company…). An enum property contributes its constants; a
method returns the options. The picked value travels in the appState of every request:
[App("Backoffice")]public class BackofficeApp{ [AppContext("Hotel")] public IReadOnlyList<OptionDto> Hotel() => hotels.Select(h => new OptionDto(h.Id, h.Name)).ToList();}Capture fields & tree selects
Section titled “Capture fields & tree selects”[Signature] renders a string property as a drawing pad (the accepted strokes land in the value
as a PNG data URI) and [PhotoCapture] as a camera capture (JPEG data URI) — no upload endpoint,
the image travels in the string. [TreeSelect(leavesOnly: true)] unfolds the field’s dropdown as
a TREE; the hierarchy comes from the view implementing IOptionsSupplier with options carrying
Children:
[UI("checkin")]public class CheckIn : IOptionsSupplier{ [Signature] public string GuestSignature { get; set; } = ""; [PhotoCapture] public string DocumentPhoto { get; set; } = ""; [TreeSelect] public string Zone { get; set; } = "";
public IReadOnlyList<Option> Options(string fieldName) => fieldName == "zone" ? [new Option("es", "Spain", [new Option("mca", "Mallorca")]), new Option("pt", "Portugal")] : [];}Database pushdown
Section titled “Database pushdown”By default Fetch() returns rows and the framework filters/sorts/paginates in memory. For real
databases override Find — run search+filter+sort+paginate as one query (count + page
inside) and the in-memory pipeline is skipped entirely:
public class Orders : Crud<Order>{ public override IEnumerable<Order> Fetch(string? s) => throw new NotSupportedException();
public override PageResult<Order>? Find( string? searchText, IReadOnlyDictionary<string, object?> filters, Pageable pageable) { var query = db.Orders.Where(...searchText, filters...); // filters = raw component var total = query.Count(); // state: <field>, and var page = query.OrderBy(...pageable.Sort...) // <field>_from/_to bounds, .Skip(pageable.Page * pageable.Size).Take(pageable.Size); // multi-selects as lists return new PageResult<Order>(page.ToList(), total); }}(The C# analogue of Java’s CrudStore.find.)
Federation (microfrontends)
Section titled “Federation (microfrontends)”Several Mateu backends compose into one shell at runtime — the frontend does the fetching, no server-side proxying:
[App("Back office")][RemoteMenu("Payments", "https://payments.example.com")] // nests the remote menu[RemoteMenu("Billing", "https://billing.example.com", Explode = true)] // inlines its entriespublic class Shell { ... }To embed a remote view as an island inside a page, put a MicroFrontend in a component tree:
public IComponent Component() => new VerticalLayout { Content = [new MicroFrontend("https://billing.example.com", "/invoices")] };The island mounts its own mateu-ux against the remote backend and runs its own sync loop.
Adapting foreign classes (component adapters)
Section titled “Adapting foreign classes (component adapters)”Java’s ComponentAdapter SPI renders third-party classes that carry no Mateu annotations. In C#
the idiomatic equivalent needs no SPI: wrap the foreign object in a view — the mapper renders
any plain properties reflectively, and your actions write back:
[UI("/pedido")]public class PedidoView // Pedido is a third-party class you cannot touch{ private readonly Pedido _pedido = PedidoRepo.Load();
public string Cliente { get => _pedido.Cliente; set => _pedido.Cliente = value; } public decimal Importe { get => _pedido.Importe; set => _pedido.Importe = value; }
[Button] public Message Guardar() { PedidoRepo.Save(_pedido); return new Message("Saved"); }}For full control of the UI, implement IComponentTreeSupplier on the wrapper instead and emit a
fluent tree.
Semantic attributes
Section titled “Semantic attributes”Any Mateu property/method attribute can decorate an attribute class, so one domain word can bundle framework configuration — the C# analogue of Java’s composed annotations, resolved transitively by the mapper (first match wins, no attribute overriding):
[AttributeUsage(AttributeTargets.Property)][Money, Label("Importe total")]public sealed class ImporteTotalAttribute : Attribute;
public class Invoice{ [ImporteTotal] public decimal Total { get; set; } // behaves as [Money] + [Label(...)]}Routing attributes ([UI], [App]) are the exception — they stay direct, like in Java.
AI chat (SSE)
Section titled “AI chat (SSE)”[AI("/ai/chat")] on the [App] class emits sseUrl in the app metadata; every renderer then
shows the floating AI chat button. The endpoint is yours to implement — the chat panel POSTs
{ "message": "user text", "sessionId": "…", "menuContext": "… (first message only)" }with Accept: text/event-stream (plus Authorization: Bearer … and X-Session-Id when
available) and renders the streamed data: lines as the reply. Special data: payloads: a JSON
{"event": "...", "detail": {...}} is re-dispatched on the client event bus (agent-error shows
an error bubble), and a token-usage JSON updates the usage footer. A minimal ASP.NET endpoint:
app.MapPost("/ai/chat", async (HttpContext ctx, ChatRq rq) =>{ ctx.Response.ContentType = "text/event-stream"; await foreach (var chunk in myAgent.StreamAsync(rq.Message, rq.SessionId)) await ctx.Response.WriteAsync($"data: {chunk}\n\n");});
public record ChatRq(string Message, string? SessionId, string? MenuContext);Status
Section titled “Status”The core Mateu surface is covered and verified live in the Compose renderer (desktop + iOS) against
this server: forms + sections + field types + validation, Crud<T> (list / detail / edit / new /
save / delete), the [App] shell + menu navigation, wizards, page decorations, i18n, events,
security scaffolding, the tail features above (tabs, stereotypes, KPIs, FABs, shortcuts, compact,
unsaved-changes guard), the nine dashboard/UX component types (MetricCard, Scoreboard,
DashboardPanel, DashboardLayout, FoldoutLayout, HeroSection, EmptyState, Skeleton, Gantt) and the
declarative page archetypes (Dashboard, Foldout, Welcome, ItemOverview). 43 golden-JSON tests
assert wire compatibility with the Java backend.
Beyond the core, the remaining Java features (component adapters, federated microfrontends, framework adapters, SSE/AI chat) follow the same pattern: extend the mapper, add a metadata DTO, add a golden test.